package org.intellij.plugins.junit4;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.refactoring.MoveClassesOrPackagesRefactoring;
import com.intellij.refactoring.MoveDestination;
import com.intellij.refactoring.RefactoringFactory;
import com.intellij.refactoring.RenameRefactoring;
import com.intellij.util.IncorrectOperationException;
import org.apache.log4j.Logger;


/**
 * Representation of a Java file in IDEA.
 *
 * @author Justin Tomich
 */
public class JavaFile
{
    private static final String JUNIT_TEST_CLASS = "org.junit.Test";

    private final Logger log = Logger.getLogger("CONSOLE-WARN");

    private final PsiJavaFile psiJavaFile;
    private final boolean isTest;
    private final PsiManager psiManager;
    private final ClassNameResolver resolver;
    private final Module module;
    private final Project project;


    public JavaFile(PsiJavaFile psiJavaFile, boolean test,
                    PsiManager psiManager, ClassNameResolver resolver)
    {
        this.psiManager = psiManager;
        this.psiJavaFile = psiJavaFile;
        this.resolver = resolver;
        this.isTest = test;

        this.module = ModuleUtil.findModuleForPsiElement(psiJavaFile);
        assert module != null;
        // todo display error window jobby if module == null
        this.project = module.getProject();
    }


    public String getQualifiedClassName()
    {
        PsiClass[] classes = psiJavaFile.getClasses();
        return classes[0].getQualifiedName();
    }


    public void reformat()
    {
        final PsiImportList imports = psiJavaFile.getImportList();
        if (imports == null) return;

        new WriteExecutor(project, "reformatting", new Runnable()
        {
            public void run()
            {
                try
                {
                    psiManager.getCodeStyleManager().reformat(imports);
                    psiManager.getCodeStyleManager().reformat(psiJavaFile);
                }
                catch (IncorrectOperationException e)
                {
                    throw new RuntimeException(e);
                }
            }
        }).run();
    }

    /**
     * Rename the JavaFile. Performs the full rename refactoring.
     *
     * @param newName new name of this JavaFile
     */
    public void rename(String newName)
    {
        log.warn("rename(" + newName + ") called on " + this);
        PsiClass psiClass = psiClass();
        RefactoringFactory rf = RefactoringFactory.getInstance(project);
        RenameRefactoring renamer = rf.createRename(psiClass, newName);
        renamer.run();
    }


    /**
     * Changes the package of the file. Performs the full move refactorin.
     *
     * @param newPackage new package of this JavaFile
     */
    public void changePackage(String newPackage)
    {
        log.warn("changePackage(" + newPackage + ") called on " + this);
        PsiClass psiClass = psiClass();
        RefactoringFactory rf = RefactoringFactory.getInstance(project);
        MoveDestination dest = rf.createSourceFolderPreservingMoveDestination(newPackage);
        MoveClassesOrPackagesRefactoring mover =
                rf.createMoveClassesOrPackages(new PsiElement[]{psiClass}, dest);
        mover.run();
    }


    public boolean isTest()
    {
        return isTest;
    }


    /**
     * Does this file have a corresponding test file?
     *
     * @return true if this file has a corresponding test
     */
    public boolean hasCorrespondingTest()
    {
        if (isTest) return false;

        String toggledName = getToggledProductionTestClassName();
        PsiClass toggledClass = findPsiClass(toggledName);
        log.debug("toggledClass=" + toggledClass);
        return toggledClass != null;
    }


    /**
     * Toggle between production and test files. That is, if this JavaFile is a production
     * class, then this method will return the corresponding test class JavaFile.
     * <p/>
     * If a production class is being toggled to a test class, and the test class does not
     * exist, the user will be asked if they wish to create the test class.
     *
     * @return production file for a test file, or test file for a production file. Null
     *         if no file to toggle to.
     */
    public JavaFile toggleProductionTestFiles()
    {
        // if we've got existing corresponding test and prod classes, find and return it.
        JavaFile toggledFile = getToggledFile();
        log.warn("JavaFile toggleProductionTestFiles() toggledFile=" + toggledFile);
        if (toggledFile != null) return toggledFile;

        // if we didn't find an existing file to toggle to, then we're toggling to a class
        //that does not exist. If we're toggling to a production class from a test, just
        // return null. We don't create production classes on demand, just test classes.
        if (isTest) return null;

        // ok, we're a production class! Awesome. Ask user for permission to create test class.
        PsiClass toggledClass = createTestClass();
        if (toggledClass == null) return null;  // if null, user bailed on creating class

        // user decided to build test class! Build our JavaFile instance, add junit Test
        // import, and return it. 
        JavaFile file = buildJavaFile(toggledClass);
        file.addTestImport();
        file.reformat();
        return file;
    }


    public String getToggledProductionTestClassName()
    {
        String toggledName = resolver.toggleName(psiJavaFile);
        log.debug("toggledName=" + toggledName);
        return toggledName;
    }


    /**
     * Open this file in an editor.
     */
    public void openInEditor()
    {
        psiJavaFile.navigate(true);
    }


    public Project getProject()
    {
        return psiJavaFile.getProject();
    }


    public String toString()
    {
        return "JavaFile{" +
                "psiJavaFile=" + psiJavaFile +
                ", resolver=" + resolver +
                ", isTest=" + isTest +
                '}';
    }

    private JavaFile getToggledFile()
    {
        String toggledName = getToggledProductionTestClassName();
        PsiClass toggledClass = findPsiClass(toggledName);
        if (toggledClass != null) return buildJavaFile(toggledClass);
        return null;
    }


    private JavaFile buildJavaFile(PsiClass psiClass)
    {
        PsiFile psiFile = psiClass.getContainingFile();
        log.debug("psiFile=" + psiFile);

        JavaFileBuilder builder = new JavaFileBuilder();
        builder.setPsiManager(psiManager);
        builder.setResolver(resolver);
        builder.setFile(psiFile.getVirtualFile());
        return builder.create();
    }

    private PsiClass psiClass()
    {
        return psiJavaFile.getClasses()[0];
    }


    private PsiClass findPsiClass(String name)
    {
        return psiManager.findClass(name, GlobalSearchScope.projectScope(project));
    }


    /**
     * Create PsiClass instance. Writes new java file to disk.
     *
     * @return created class, or null if the user bailed out.
     */
    private PsiClass createTestClass()
    {
        String qualifiedClassName = getToggledProductionTestClassName();
        String pkg = resolver.packageOnly(qualifiedClassName);
        String nameOnly = resolver.nameOnly(qualifiedClassName);
        CreateTestClassCommand createClassCommand =
                new CreateTestClassCommand(nameOnly, pkg, psiManager, module);
        ApplicationManager.getApplication().runWriteAction(createClassCommand);
        return createClassCommand.getCreatedClass();
    }

    private void addTestImport()
    {
        GlobalSearchScope scope = GlobalSearchScope.allScope(project);
        final PsiClass testClass = psiManager.findClass(JUNIT_TEST_CLASS, scope);

        if (testClass == null)
        {
            String msg = "Cannot import " + JUNIT_TEST_CLASS + ". Add junit to project.";
            Dialog.error(project, msg);
            return;
        }

        Runnable task = new Runnable()
        {
            public void run()
            {
                psiJavaFile.importClass(testClass);
            }
        };

        WriteExecutor.run(project, "adding import:" + JUNIT_TEST_CLASS, task);
    }

}
